其他
Java内存模型之可见性(填坑之路)
(给ImportNew加星标,提高Java技能)
转自:简书 作者:徐志毅
www.jianshu.com/p/6abcddd04f4e
java version "1.8.0_172" Java(TM) SE Runtime Environment (build 1.8.0_172-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.172-b11, mixed mode)
基本概念
Java内存模型
volatile关键字
1)保证被volatile修饰的共享变量对所有线程 总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
2)禁止指令重排序优化。
可见性
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test1 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
}
System.out.printf("**********test1 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.printf("**********test1 main thread 结束, i=%d **********\n", i);
}
}
示例2:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test2 {
private static volatile boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
}
System.out.printf("**********test2 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.printf("**********test2 main thread 结束, i=%d **********\n", i);
}
}
实战
让没有volatile也能跳出循环
方式一
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test3 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
}
System.out.printf("**********test3 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(1);
flag = false;
System.out.printf("**********test3 main thread 结束, i=%d **********\n", i);
}
}
**********test3 main thread 结束, i=60167 **********
**********test3 跳出成功, i=60167 **********
当主线程停顿时间很极短(1~2ms)时,可以跳出循环; 当主线程停顿时间较长时,无法跳出循环;
当子线程循环执行时间极短(1~2ms)时,可以跳出循环; 当子线程循环执行时间较长时,无法跳出循环;
当子线程循环次数较少时,可以跳出循环; 当子线程循环次数较多时,无法跳出循环;
当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是 JIT 编译器。
运行过程中会被即时编译器编译的“热点代码”有两类:
1)被多次调用的方法。
2)被多次调用的循环体。
-Xint :强制使用解释执行的方式启动java虚拟机,此模式下,不会使用JIT优化,示例1和示例3的代码都会跳出循环。 -Xcomp:强制使用编译执行的方式启动java虚拟机,此模式下,代码会被优化并编译成机器码,示例1和示例3都无法填出循环。
方式二
示例4:
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test4 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
i++;
System.out.println("i=" + i);
}
System.out.printf("**********test4 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
flag = false;
System.out.printf("**********test4 main thread 结束, i=%d **********\n", i);
}
}
为了找出原因,我对print代码进行了几次不同的替换:
package com.youzan;
import java.util.HashMap;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test5 {
private static boolean flag = true;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag) {
doSomeThing1();
}
System.out.printf("**********test4 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(10);
flag = false;
System.out.printf("**********test4 main thread 结束, i=%d **********\n", i);
}
private static void doSomeThing1() {
System.out.println("doSomeThing1");
}
private static void doSomeThing2() {
synchronized (Test5.class) {
i++;
}
}
private static void doSomeThing3() {
i++;
Thread.yield();
}
private static void doSomeThing4() {
new HashMap<>();
}
}
根据java的内存模型规范,一个线程对普通变量的修改并不需要立即写回到主存,且另一个线程读取也不需要每一次都从主存中去读取。至于什么时候与主内存同步,虚拟机只需保证方法出栈时将修改的值同步到主内存。因此这其中有比较宽松的优化空间。而上述几个方法,都存在一定的同步空间。虚拟机会在此时与主内存同步。
volatile的传播范围
把volatile对象传递给另一个对象,新对象是否立即可见呢? 当volatile修饰对象时,如果对象的嵌套的层级较深,那该对象的内部是否立即可见呢?
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test6 {
private static volatile ReferenceFlag referenceFlag = new ReferenceFlag();
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
BaseFlag baseFlag = referenceFlag.baseFlag;
while (baseFlag.flag) {
i++;
}
System.out.printf("**********test6 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
referenceFlag.baseFlag.flag = false;
System.out.printf("**********test6 main thread 结束, i=%d **********\n", i);
}
static class BaseFlag {
boolean flag = true;
}
static class ReferenceFlag {
volatile BaseFlag baseFlag = new BaseFlag();
}
}
在示例6中,使用了引用嵌套的方式来验证volatile是否可以传递给一个局部变量,示例中的引用都是用来volatile关键字来修饰,运行结果是无法跳出。
结论一:当使用一个变量来接受一个volatile修饰的变量时,volatile的可见性并不会传递。即新的变量不再具有volatile特性。
package com.youzan;
/**
* Date: 2018/8/12
* @author xuzhiyi
*/
public class Test7 {
private static int i = 0;
private static volatile DeapReferenceInnerFlag deapReferenceInnerFlag = new DeapReferenceInnerFlag();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (deapReferenceInnerFlag.referenceInnerFlag.baseFlag.flag) {
i++;
}
System.out.printf("**********test7 跳出成功, i=%d **********\n", i);
});
thread.start();
Thread.sleep(100);
deapReferenceInnerFlag.referenceInnerFlag.baseFlag.flag = false;
System.out.printf("**********test7 main thread 结束, i=%d **********\n", i);
}
static class BaseFlag {
boolean flag = true;
}
static class ReferenceInnerFlag {
BaseFlag baseFlag = new BaseFlag();
}
static class DeapReferenceInnerFlag {
ReferenceInnerFlag referenceInnerFlag = new ReferenceInnerFlag();
}
}
ps:结论二没有很好的理论依据,仅从实践上看是如此。
总结
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
好文章,我在看❤️